<!-- Licensed under a BSD license. See license.html for license -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Environment Mapping Diagram</title>
<style>
:root {
  color-scheme: light dark;
}
html, body {
  margin: 0;
  height: 100%;
}
canvas {
  width: 100%;
  height: 100%;
  display: block;
  cursor: crosshair;
}
@media (prefers-color-scheme: dark) {
  canvas {
    background: #444;
  }
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
<script type="module">
import GUI from '../../../3rdparty/muigui-0.x.module.js';
import * as twgl from '../../../3rdparty/twgl-full.module.js';
import * as diagram from './diagram.js';
import { rgb, hsl } from './utils.js';
const { v3 } = twgl;

function main() {
  const opt = getQueryParams();
  const lang = {
    dot: opt.dot || 'dot(eyeToSurfaceDir,surfaceNormal)',
    surfaceNormal: opt.surfaceNormal || 'surface normal',
  };
  const ctx = document.querySelector('#canvas').getContext('2d');
  let direction = 0;
  const surfaceWidth = 200;

  const gradientLabels = [
    { label: '-x', rotation: Math.PI * 1.5, },
    { label: '+y', rotation: Math.PI, },
    { label: '+x', rotation: Math.PI * .5, },
    { label: '-y', rotation: 0, },
  ];
  const envGradients = [];
  for (let i = 0; i < 4; ++i) {
    const grad = ctx.createLinearGradient(-125, 0, 125, 0);
    for (let j = 0; j <= 20; ++j) {
      grad.addColorStop(1 - j / 20, hsl((i + j / 20) / 4, 1, .5));
    }
    envGradients.push(grad);
  }

  function degToRad(deg) {
    return deg * Math.PI / 180;
  }

  const data = {
    rotation: 20,
    normals: true,
    eyeToSurface: true,
    addedVectors: true,
    reflection: true,
    multipleRays: false,
  };
  const gui = new GUI();
  GUI.setTheme('float');
  gui.onChange(render);
  gui.add(data, 'rotation', {min: -70, max: 70});
  gui.add(data, 'multipleRays');
  gui.add(data, 'normals');
  gui.add(data, 'eyeToSurface');
  gui.add(data, 'addedVectors');
  gui.add(data, 'reflection');

  const darkColors = {
    base: '#DDD',
    background: '#444',
    text: '#FFF',
    textOutline: '#000',
    eyeToSurface: '#555',
  };
  const lightColors = {
    base: '#000',
    background: '#FFF',
    text: '#000',
    textOutline: '#FFF',
    eyeToSurface: '#F8F8F8',
  };

  const darkMatcher = window.matchMedia('(prefers-color-scheme: dark)');
  darkMatcher.addEventListener('change', render);

  function render() {
    const isDarkMode = darkMatcher.matches;
    const colors = isDarkMode ? darkColors : lightColors;

    direction = degToRad(data.rotation);

    twgl.resizeCanvasToDisplaySize(ctx.canvas, window.devicePixelRatio);
    const width  = 250;
    const height = 250;

    const baseScale = Math.min(ctx.canvas.width / width,  ctx.canvas.height / height);

    ctx.fillStyle = colors.background;
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    ctx.save();
    ctx.translate(ctx.canvas.width / 2, ctx.canvas.width / 2);
    ctx.scale(baseScale, baseScale);

    ctx.font = '8px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';

    const lx = 0;
    const ly = -height / 3;

    const sx = 0;
    const sy = height / 5;

    const dots = [];
    const numArrows = data.multipleRays ? 6 : 1;
    for (let ii = 0; ii < numArrows; ++ii) {
      const u = numArrows > 1
         ? (ii / (numArrows - 1))
         : .5;
      const r = (u * 2 - 1) * 70;
      const c = Math.cos(direction);
      const s = Math.sin(direction);
      const x = c * r;
      const y = s * r + sy;
      const a = -Math.atan2(lx - x, ly - y);

      if (data.eyeToSurface) {
        ctx.fillStyle = colors.eyeToSurface;
        ctx.strokeStyle = colors.eyeToSurface;
        diagram.arrow(ctx, lx, ly, x, y, false, true, 0.5);

        ctx.save();
        ctx.translate(x, y);
        ctx.rotate(a);
        ctx.fillStyle = '#000';
        ctx.strokeStyle = '#000';
        diagram.arrow(ctx, 0, 5, 0, 40, true, false, 0.5);
        ctx.restore();
      }

      const eyeToSurfaceDir = [Math.sin(a), Math.cos(a)];
      const surfaceNormal = [Math.sin(direction), Math.cos(direction)];
      const dot = v3.dot([...eyeToSurfaceDir, 0], [...surfaceNormal, 0]);

      const snScale = 40;
      const snx = x + -surfaceNormal[0] * snScale * dot;
      const sny = y +  surfaceNormal[1] * snScale * dot;

      const snx2 = snx + -surfaceNormal[0] * snScale * dot;
      const sny2 = sny +  surfaceNormal[1] * snScale * dot;

      const lex = x + -eyeToSurfaceDir[0] * snScale;
      const ley = y +  eyeToSurfaceDir[1] * snScale;

      if (data.normals) {
        ctx.fillStyle = '#080';
        ctx.strokeStyle = '#080';
        diagram.arrow(ctx, x, y, snx, sny, false, true, 0.5);
        diagram.arrow(ctx, snx, sny, snx2, sny2, false, true, 0.5);
      }

      if (data.addedVectors) {
        ctx.fillStyle = '#FCC';
        ctx.strokeStyle = '#FCC';
        diagram.arrow(ctx, lex, ley, snx2, sny2, false, true, 0.3);
      }

      const minusOneDot = [];
      // eyeToSurfaceDir – 2 ∗ dot(surfaceNormal, eyeToSurfaceDir) ∗ surfaceNormal
      const reflect = eyeToSurfaceDir.map((incident, ndx) => {
        const normal = surfaceNormal[ndx];
        minusOneDot[ndx] = incident - dot * normal;
        return incident - 2 * dot * normal;
      });

      const ra = Math.atan2(...reflect) + Math.PI;
      {
        ctx.save();
        ctx.translate(-85, -85);
        ctx.scale(.25, .25);
        ctx.fillStyle = '#F00';
        ctx.strokeStyle = '#F00';
        const exToEdge = 110 / Math.max(Math.abs(reflect[0]), Math.abs(reflect[1]));
        const ex = reflect[0] * exToEdge;
        const ey = -reflect[1] * exToEdge;
        diagram.arrow(ctx, 0, 0, ex, ey, false, true, 0.5);
        ctx.restore();
      }
      if (data.reflection) {
        ctx.save();
        ctx.translate(x, y);
        ctx.rotate(ra);
        //ctx.fillStyle = "#F8F8FF";
        //ctx.strokeStyle = "#F8F8FF";
        //diagram.arrow(ctx, 0, 0, 0, 300, false, false, 0.5);
        ctx.fillStyle = '#F00';
        ctx.strokeStyle = '#F00';
        diagram.arrow(ctx, 0, 0, 0, 40, false, true, 0.5);
        ctx.restore();
      }

      dots.push({
        dot,
        r: r,
      });
    }

    {
      ctx.strokeStyle = 'black';
      ctx.fillStyle = 'brown';
      diagram.drawEye(ctx, lx, ly, width / 10, height / 10);

      ctx.save();
      ctx.translate(sx, sy);
      ctx.rotate(direction);

      const gradient = ctx.createLinearGradient(-surfaceWidth / 2, 0, surfaceWidth / 2, 0);
      const numPoints = 5;
      for (let p = 0; p <= numPoints; ++p) {
        const u = p / numPoints;
        const r = (u * 2 - 1) * surfaceWidth * .5;
        const c = Math.cos(direction);
        const s = Math.sin(direction);
        const x = c * r;
        const y = s * r + sy;
        const a = -Math.atan2(lx - x, ly - y);

        const eyeToSurfaceDir = [-Math.sin(a), -Math.cos(a)];
        const surfaceNormal = [Math.sin(direction), Math.cos(direction)];
        const dot = v3.dot([...eyeToSurfaceDir, 0], [...surfaceNormal, 0]);

        const minusOneDot = [];
        const reflect = eyeToSurfaceDir.map((incident, ndx) => {
          const normal = surfaceNormal[ndx];
          minusOneDot[ndx] = incident - dot * normal;
          return incident - 2 * dot * normal;
        });

        let gradNdx = 0;
        let sc = reflect[1];
        if (Math.abs(reflect[1]) > Math.abs(reflect[0])) {
          if (reflect[1] > 0) {
            gradNdx = 3;
            sc = -reflect[0];
          } else  {
            gradNdx = 1;
            sc = reflect[0];
          }
        } else if (reflect[0] < 0) {
          gradNdx = 2;
          sc = -reflect[1];
        }
        const ma = Math.max(Math.abs(reflect[0]), Math.abs(reflect[1]));
        const t = 1 - .5 * (sc / ma + 1);

        gradient.addColorStop(u, hsl((gradNdx + t) / 4, 1, .5));
      }
      ctx.fillStyle = gradient;

      // draw surface
      diagram.roundedRect(ctx, surfaceWidth * -0.5, 0, surfaceWidth, 20);

      //var d  = m3.dot(0, 1, Math.sin(direction), Math.cos(direction));
      //ctx.fillStyle = 'rgba(0,255,0,.2)';//rgb(1,0,0);

      ctx.fill();
      ctx.strokeStyle = rgb(0, 0, 0);
      ctx.stroke();

      ctx.fillStyle = '#000';
      ctx.fillText(lang.dot, 0, 14);

      dots.forEach(function(dot) {
          const r = dot.r;
          ctx.save();
          ctx.translate(r, 0);
          ctx.font = '5px sans-serif';
          ctx.fillText(dot.dot.toFixed(3), 0, 5);
          ctx.restore();
      });

      ctx.restore();
    }

    {
      ctx.save();
      ctx.translate(-85, -85);
      ctx.scale(.25, .25);
      envGradients.forEach((grad, ndx) => {
        ctx.save();
          ctx.rotate(Math.PI + (ndx - 1) / 2 * Math.PI);
          ctx.translate(0, 125);
          ctx.fillStyle = grad;
          ctx.fillRect(-125, -10, 250, 20);
          ctx.strokeStyle = colors.textOutline;
          ctx.fillStyle = colors.text;
          ctx.translate(0, 15);
          ctx.scale(4, 4);
          ctx.rotate(gradientLabels[ndx].rotation);
          //ctx.fillText(gradientLabels[ndx].label, 0, 0);
          diagram.outlineText(ctx, gradientLabels[ndx].label, 0, 0);
        ctx.restore();
      });
      ctx.restore();
    }

    ctx.restore();
  }
  render();
}

function getQueryParams() {
  const params = {};
  if (window.location.search) {
    window.location.search.substring(1).split('&').forEach(function(pair) {
      const keyValue = pair.split('=').map(function(kv) {
        return decodeURIComponent(kv);
      });
      params[keyValue[0]] = keyValue[1];
    });
  }
  return params;
}

main();
</script>

